using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;

using gisgeb.plugins.interfaces;
using Microsoft.Win32;

namespace gisgeb.comserver
{
    [Guid("DBE0D00F-A216-4586-9E8C-C16CBD363CAB")]
    [ComVisible(true)]
    public class GisgebContextMenu: IShellExtInit, IContextMenu
    {
        #region P/Invokes
        /// <summary>
        /// Inserts a new menu item at the specified position in a menu.
        /// </summary>
        /// <param name="hmenu">A handle to the menu in which the new menu item is inserted. </param>
        /// <param name="uposition">The identifier or position of the menu item before which to insert the new item.
        /// The meaning of this parameter depends on the value of uflags.</param>
        /// <param name="uflags">Controls the meaning of uposition. If this parameter is FALSE,
        /// uposition is a menu item identifier. Otherwise, it is a menu item position.</param>
        /// <param name="mii">A pointer to a MENUITEMINFO structure that contains information about the new menu item. </param>
        /// <returns>If the function succeeds, the return value is nonzero.
        /// If the function fails, the return value is zero. To get extended error information,
        /// use the Marshal.GetLastWin32Error function.</returns>
        [DllImport("user32.dll", EntryPoint="InsertMenuItem", SetLastError=true)]
        private static extern int InsertMenuItemInternal(IntPtr hmenu, uint uposition, uint uflags, ref Menuiteminfo mii);

        private void InsertMenuItem(IntPtr hmenu, uint uposition, uint uflags, ref Menuiteminfo mii)
        {
            int ok = GisgebContextMenu.InsertMenuItemInternal(hmenu, uposition, uflags, ref mii);
            if(ok == 0)
            {
                int errorcode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorcode);
            }
        }
        
        [DllImport("kernel32.dll", EntryPoint = "lstrcpynW")]
        internal static extern IntPtr LstrCpynW(IntPtr lpString1, IntPtr lpString2, int iMaxLength);

        [DllImport("kernel32.dll", EntryPoint = "lstrcpynA")]
        internal static extern IntPtr LstrCpynA(IntPtr lpString1, IntPtr lpString2, int iMaxLength);

        /// <summary>
        /// Creates a drop-down menu, submenu, or shortcut menu
        /// <see cref="http://msdn.microsoft.com/en-us/library/ms647626%28VS.85%29.aspx" />
        /// </summary>
        /// <returns>If the function succeeds, the return value is a handle to the newly created menu.
        /// If the function fails, the return value is NULL.
        /// To get extended error information, call Marshal.GetLastWin32Error.</returns>
        [DllImport("user32.dll", EntryPoint = "CreatePopupMenu", SetLastError=true)]
        internal static extern IntPtr CreatePopupMenuInternal();

        private IntPtr CreatePopupMenu()
        {
            IntPtr menuPointer = CreatePopupMenuInternal();
            if(menuPointer == IntPtr.Zero)
            {
                int errorcode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorcode);
            }
            
            return menuPointer;
        }
        
        #endregion

        private const string GUID = "{DBE0D00F-A216-4586-9E8C-C16CBD363CAB}";

        private const string NAME = "gisgeb Explorer-Extension";

        private Hdrop hDrop;

        private Dictionary<int, IMenuEntryPlugin> menuItems;

        #region IContextMenu

        int IContextMenu.QueryContextMenu(IntPtr hMenu, uint iMenu, int idCmdFirst, int idCmdLast, uint uFlags)
        {
            try
            {
                // Create the popup to insert
                IntPtr hmnuPopup = this.CreatePopupMenu();

                uint id = 1;
                if ( (uFlags & 0xf) == 0 || (uFlags & (uint)Cmf.CmfExplore) != 0)
                {
                    // Populate the popup menu with file-specific items
                    id = PopulateMenu(hmnuPopup, (uint)idCmdFirst+ id);
                    
                    // Add the popup to the context menu
                    Menuiteminfo mii = new Menuiteminfo
                                       {
                                           cbSize = (uint)Marshal.SizeOf(typeof(Menuiteminfo)),
                                           fMask = (uint) Miim.Type | (uint) Miim.State | (uint) Miim.Submenu,
                                           hSubMenu = hmnuPopup,
                                           fType = (uint) Mf.String,
                                           dwTypeData = "Programming",
                                           fState = (uint) Mf.Enabled
                                       };

                    this.InsertMenuItem(hMenu, iMenu, 1, ref mii);
                    
                    // Add a separator
                    Menuiteminfo sep = new Menuiteminfo
                                       {
                                           cbSize = (uint)Marshal.SizeOf(typeof(Menuiteminfo)),
                                           fMask = (uint) Miim.Type,
                                           fType = (uint) Mf.Separator
                                       };
                    this.InsertMenuItem(hMenu, iMenu+1, 1, ref sep);
                    
                }

                return HResultHelper.MakeHresult(HResultHelper.Severity.Success,HResultHelper.Faculty.Null, (int)id);
            }
            catch (Exception e)
            {
                this.ShowError(e);
                return HResultHelper.MakeHresult(HResultHelper.Severity.Error, HResultHelper.Faculty.Null, 2);
            }            
        }

        private void AddMenuItem(IntPtr hMenu, string text, uint id, int position)
        {
            Menuiteminfo mii = new Menuiteminfo
                               {
                                   cbSize = (uint)Marshal.SizeOf(typeof(Menuiteminfo)),
                                   fMask = (uint) Miim.ID | (uint) Miim.Type | (uint) Miim.State,
                                   wID = id,
                                   fType = (uint) Mf.String,
                                   dwTypeData = text,
                                   fState = (uint) Mf.Enabled
                               };
            this.InsertMenuItem(hMenu, checked((uint)position), 1, ref mii);
        }

        uint PopulateMenu(IntPtr hMenu, uint id)
        {            
            foreach(KeyValuePair<int, IMenuEntryPlugin> entry in this.menuItems)
            {
                this.AddMenuItem(hMenu, entry.Value.MenuDisplayText, id++, entry.Key);
            }

            return id;
        }

        void IContextMenu.GetCommandString(int idCmd, uint uFlags, int pwReserved, IntPtr commandString, int cchMax)
        {
            IMenuEntryPlugin entry = this.menuItems[idCmd-1];
            string helpText = entry.HelpText;
            
            if (helpText.Length >= cchMax) helpText = helpText.Substring(0, cchMax-1);
            if((uFlags  & (uint)Gcs.Unicode) != 0 )
            {
                IntPtr tmpPtr = Marshal.StringToHGlobalUni(helpText);
                LstrCpynW(commandString, tmpPtr, cchMax);
                Marshal.FreeHGlobal(tmpPtr);
            }
            else
            {
                IntPtr tmpPtr = Marshal.StringToHGlobalAnsi(helpText);
                LstrCpynA(commandString, tmpPtr, cchMax);
                Marshal.FreeHGlobal(tmpPtr);
            }
            
        }
        
        void IContextMenu.InvokeCommand (IntPtr pici)
        {
            try
            {
                Invokecommandinfo ici = (Invokecommandinfo)Marshal.PtrToStructure(pici, typeof(Invokecommandinfo));

                //TODO: Either only one folder, or the Enumerable has to be part of MenuEntry...
                IEnumerable<string> folders = this.hDrop == null 
                                          ? new[] {ici.directory} 
                                          : this.hDrop.GetFileNames();
                int id = ici.verb - 1;
                IMenuEntryPlugin entry = this.menuItems[id];
                foreach(string folder in folders)
                {
                    entry.Command(folder);
                }
            }
            catch(Exception e)
            {
                this.ShowError(e);
                throw;
            }
        }
        #endregion

        #region IShellExtInit
        void IShellExtInit.Initialize (IntPtr pidlFolder, IntPtr lpdobj, uint hKeyProgID)
        {
            try
            {
                if (lpdobj != (IntPtr)0)
                {
                    // Get info about the directory
                    IDataObject dataObject = (IDataObject)Marshal.GetObjectForIUnknown(lpdobj);
                    Formatetc fmt = new Formatetc
                                    {
                                        cfFormat = Clipformat.CfHdrop,
                                        ptd = 0,
                                        dwAspect = Dvaspect.DvaspectContent,
                                        lindex = -1,
                                        tymed = Tymed.TymedHglobal
                                    };
                    Stgmedium medium = new Stgmedium();
                    dataObject.GetData(ref fmt, ref medium);
                    this.hDrop = new Hdrop(medium.hGlobal);
                }
                else
                {
                    this.hDrop = null;
                }
                
                this.InitMenuEntries();
            }
            catch(Exception e)
            {
                ShowError(e);
                throw;
            }
        }

        private void InitMenuEntries()
        {
            try
            {
                this.menuItems = new Dictionary<int, IMenuEntryPlugin>();
                string installFolder = Path.GetDirectoryName(this.GetType().Assembly.Location);
                if (installFolder == null)
                {
                    return;
                }

                Config config = null;
                try
                {
                    config = Config.LoadFromFolder(installFolder);
                }
                catch(ConfigurationFileException e)
                {
                    MessageBox.Show(
                        "Error in configuration.\nThe Configuration will be ignored.\n\nThe error was: " + e.Message,
                        "Configuration error",
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Error);
                    config = new Config();
                }

                string pluginsFolder = Path.Combine(installFolder, "Plugins");
                if(!Directory.Exists(pluginsFolder))
                {
                    return;
                }
                
                string[] pluginDlls = Directory.GetFiles(pluginsFolder, "*.dll");
                IList<IMenuEntryPlugin> possiblePlugins = new List<IMenuEntryPlugin>();
                //MessageBox.Show(pluginDlls.Aggregate("plugins:", (current, next) => current + "\n" + next));
                //MessageBox.Show(config.IgnoredPluginDllNames.Aggregate("ignored:", (current, next) => current + "\n" + next));
                foreach(string pluginDll in pluginDlls)
                {
                    if(this.IsPluginIgnored(pluginDll, config.IgnoredPluginDllNames))
                    {
                        continue;
                    }
                    
                    Assembly pluginAssembly = Assembly.LoadFrom(pluginDll);
                    foreach(Type plugin in pluginAssembly.GetTypes())
                    {
                        if(plugin.GetInterface(typeof(IMenuEntryPlugin).Name) != null &&
                            !plugin.IsAbstract)
                        {
                                IMenuEntryPlugin pluginInstance = (IMenuEntryPlugin)Activator.CreateInstance(plugin);
                                possiblePlugins.Add(pluginInstance);

                        }
                    }
                }
                
                int position = 0;
                foreach(IMenuEntryPlugin plugin in possiblePlugins.OrderBy(p => p.PositionHint))
                {
                    this.menuItems.Add(position++, plugin);
                }
            }
            catch (ReflectionTypeLoadException e)
            {
                foreach(Exception inner in e.LoaderExceptions)
                {
                    this.ShowError(inner);
                }
                throw ;
            }
            catch (Exception e)
            {
                this.ShowError(e);
                throw;
            }
        }
        
        private bool IsPluginIgnored(string pluginDll, List<string> ignoredPluginDllNames)
        {
            return ignoredPluginDllNames.Count(p => pluginDll.EndsWith(p, StringComparison.OrdinalIgnoreCase)) > 0;
        }

        void ShowError(Exception e)
        {
            MessageBox.Show(e.GetType().Name + "\n " + e.Message, "Fehler in " + NAME);
        }

        #endregion
        
        #region Registration
        [System.Runtime.InteropServices.ComRegisterFunctionAttribute]
        static void RegisterServer(String str1)
        {
            try
            {
                RegistryKey root = Registry.LocalMachine;
                RegistryKey rk = root.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved", true);
                if(rk == null)
                {
                    return;
                }

                rk.SetValue(GUID, NAME);
                rk.Close();
                
                root = Registry.ClassesRoot;
                foreach(string place in new[] {"Directory", "Directory\\Background"})
                {
                    rk = root.CreateSubKey(string.Format("{0}\\shellex\\ContextMenuHandlers\\{1}", place, NAME));
                    if (rk == null)
                    {
                        continue;
                    }

                    rk.SetValue("", GUID);
                    rk.Close();
                }
            }
            catch(Exception e)
            {
                System.Console.WriteLine(e.ToString());
            }
        }

        [System.Runtime.InteropServices.ComUnregisterFunctionAttribute]
        static void UnregisterServer(String str1)
        {
            try
            {
                // Remove ShellExtenstions registration
                RegistryKey root = Registry.LocalMachine;
                RegistryKey rk = root.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Shell Extensions\\Approved", true);
                if(rk == null)
                {
                    return;
                }

                rk.DeleteValue(GUID);
                rk.Close();

                // Delete  regkey
                root = Registry.ClassesRoot;
                foreach(string place in new[] {"Directory", "Directory\\Background"}) //{"BatchResults"})
                {
                    root.DeleteSubKey(string.Format("{0}\\shellex\\ContextMenuHandlers\\{1}", place, NAME));
                }
            }
            catch(Exception e)
            {
                System.Console.WriteLine(e.ToString());
            }
        }
        #endregion

    }
}
